--------------Galaxy Gates-------------
A 4am crack                  2018-04-06
-------------------. updated 2018-04-07
                   |___________________

Name: Galaxy Gates
Genre: arcade
Year: 1981
Credits: Eric Popejoy
Publisher: Magna Soft
Platform: Apple ][+ or later
Media: 5.25-inch disk
Sides: 1
OS: DOS 3.2

                   ~

               Chapter 0
     If You Give a Cracker a Disk


Passport automatically converted this
disk from 13-sector to 16-sector.
Here is the transcript of that part:

                 --v--

READING FROM S6,D1
T00,S00 FOUND DOS 3.2 BOOTLOADER
USING DISK'S OWN RWTS
WRITING TO S5,D2
T00,S02 WRITING BUILT-IN RWTS
T00,S01,$38: 20 -> 2C
T00,S00 WRITING STANDARD DELIVERY
BOOTLOADER
CRACK COMPLETE. PRESS ANY KEY

                 --^--

[Narrator]
But the crack was not complete.

When I boot the 16-sector copy that
Passport produces, it gets as far as
loading the text title screen, shows a
bit more disk activity, then fills the
screen with inverse "R" and hangs.

The plot thickens.

                   ~

               Chapter 1
     If You Give a Pirate a Prompt


With a well-timed <Ctrl-Reset> during
boot, immediately after seeing the "]"
prompt, my non-working copy drops me to
a working prompt. It appears that DOS
is still in memory. Let's see if I can
use that to my advantage.

]CATALOG

DISK VOLUME 254

 B 061 GALAXY GATES

]BLOAD GALAXY GATES
...fills screen with title title screen
   but eventually drops me back to the
   prompt...

Ah, interesting. The text title screen
is actually part of the file.

]CALL -151

; last BLOAD address
*AA71.AA72

AA72- FD 03

*3FDL

03FD-   4C 00 3A    JMP   $3A00

*3A00L

3A00-   4C 09 3A    JMP   $3A09

*3A09L

3A09-   A2 00       LDX   #$00
3A0B-   8A          TXA
3A0C-   9D 00 07    STA   $0700,X
3A0F-   CA          DEX
3A10-   D0 FA       BNE   $3A0C
3A12-   A9 01       LDA   #$01
3A14-   85 B0       STA   $B0
3A16-   4C 00 28    JMP   $2800

*2800L

; get address of RWTS parameter table
2800-   20 E3 03    JSR   $03E3

; get last used slot from RWTS table
2803-   84 00       STY   $00
2805-   85 01       STA   $01
2807-   A0 0F       LDY   #$0F
2809-   B1 00       LDA   ($00),Y
280B-   AA          TAX

; turn on drive motor
280C-   BD 89 C0    LDA   $C089,X

Aha! I do believe we have now arrived
at The Fun Part.

                   ~

               Chapter 2
             The Fun Part


Turning on the drive motor manually in
the middle of a program (and not, say,
deep within the RWTS where such a thing
is legitimately required) can only mean
one thing: shenanigans.

*280FL

; wait for drive to spin up
280F-   A0 18       LDY   #$18
2811-   BD 8C C0    LDA   $C08C,X
2814-   30 05       BMI   $281B
2816-   88          DEY
2817-   D0 F8       BNE   $2811

; failure to spin up means something is
; desperately wrong, like a pirate is
; trying to boot trace or something, so
; give up immediately
2819-   F0 1A       BEQ   $2835

; Death Counters, probably
281B-   A9 00       LDA   #$00
281D-   8D 65 28    STA   $2865
2820-   A9 10       LDA   #$10
2822-   8D 66 28    STA   $2866
2825-   A9 04       LDA   #$04
2827-   8D 64 28    STA   $2864
282A-   78          SEI
282B-   CE 65 28    DEC   $2865
282E-   D0 37       BNE   $2867
2830-   CE 66 28    DEC   $2866
2833-   D0 32       BNE   $2867

; failure falls through to here --
; turn off the drive motor
2835-   BD 88 C0    LDA   $C088,X

; copy the next chunk of code to lower
; memory ($200 to be precise)
2838-   A0 15       LDY   #$15
283A-   A9 00       LDA   #$00
283C-   85 01       STA   $01
283E-   A9 02       LDA   #$02
2840-   85 00       STA   $00
2842-   B9 4D 28    LDA   $284D,Y
2845-   91 00       STA   ($00),Y
2847-   88          DEY
2848-   10 F8       BPL   $2842

; and continue from there
284A-   6C 00 00    JMP   ($0000)

; (executed at $200)
; wipe all memory
284D-   A9 00       LDA   #$00
284F-   85 00       STA   $00
2851-   A8          TAY
2852-   A9 04       LDA   #$04
2854-   85 01       STA   $01

; inverse "R" character, which is what
; I saw on my non-working copy
2856-   A9 12       LDA   #$12
2858-   91 00       STA   ($00),Y
285A-   E6 00       INC   $00
285C-   D0 FA       BNE   $2858
285E-   E6 01       INC   $01
2860-   D0 F6       BNE   $2858
2862-   58          CLI
2863-   60          RTS

The actual protection check continues
at $2867, from the branches at $282E
and $2833.

; find a $D5 nibble
2867-   BD 8C C0    LDA   $C08C,X
286A-   10 FB       BPL   $2867
286C-   C9 D5       CMP   #$D5
286E-   D0 BB       BNE   $282B

; skip several nibbles
2870-   A0 0C       LDY   #$0C
2872-   EA          NOP
2873-   BD 8C C0    LDA   $C08C,X
2876-   10 FB       BPL   $2873
2878-   88          DEY
2879-   D0 F7       BNE   $2872

; find an $AA nibble (if we started at
; the start of an address prologue,
; this nibble will be part of the
; address epilogue)
287B-   C9 AA       CMP   #$AA
287D-   D0 AC       BNE   $282B
287F-   F0 00       BEQ   $2881

; fetch from data latch -- note: no BPL
; loop here, we're just getting one
; value from the data latch
2881-   BD 8C C0    LDA   $C08C,X

; if data latch is >= 8 at this point,
; fail (well, start over and try again)
2884-   C9 08       CMP   #$08
2886-   B0 A3       BCS   $282B

; match the rest of the epilogue
2888-   BD 8C C0    LDA   $C08C,X
288B-   10 FB       BPL   $2888
288D-   C9 EB       CMP   #$EB
288F-   D0 9A       BNE   $282B
2891-   BD 8C C0    LDA   $C08C,X
2894-   10 FB       BPL   $2891
2896-   C9 FD       CMP   #$FD
2898-   D0 91       BNE   $282B
289A-   CE 64 28    DEC   $2864
289D-   D0 C8       BNE   $2867

; turn off drive motor and continue to
; the actual game code
289F-   BD 88 C0    LDA   $C088,X
28A2-   58          CLI
28A3-   4C 00 20    JMP   $2000

The LDA / CMP at $2881 are the heart of
the protection check. To understand
why, we get to take a little voyage.

                   ~

               Chapter 3
          The Really Fun Part


Each bit on disk takes 4 CPU cycles to
come around as the disk is spinning.
The data latch is the softswitch in the
Apple II memory ($C0EC for slot 6, but
usually written as "$C08C,X" to be
slot-independent) that corresponds to
the "current" value that's been read
from the disk so far. Normally you just
poll the data latch until the high bit
goes on, at which point you have a full
nibble. That's why most RWTS code has
an LDA/BPL loop that looks something
like this:

-   BD 8C C0    LDA   $C08C,X
    10 FB       BPL   -

However, you aren't required to poll
the data latch constantly. It acts as a
micro-cache, keeping the "current"
value as bits go flying by, whether
you're polling it or not.

For example, the epilogue on this disk
is "DE AA EB", with a timing bit after
the "AA". So the bitstream looks like
this:

   110111101010101001110101111111101
   |--DE--||--AA--| |--EB--||--FD--|
                   ^
            extra timing bit

As the disk spins(*), these bits are
shifted into the data latch at a rate
of 1 bit every 4 CPU cycles.

(*) I still maintain that "As The Disk
    Spins" would make a great name for
    a retrocomputing-themed soap opera.

Let's look at the bits in and around
that timing bit, starting with the $AA
nibble:

Time | <-- as the disk spins |  $C0EC
-----+-----------------------+---------
 -28 | .......10101010011101 | 00000001
-----+--------^--------------+---------
 -24 | ......101010100111010 | 00000010
-----+--------^--------------+---------
 -20 | .....1010101001110101 | 00000101
-----+--------^--------------+---------
 -16 | ....10101010011101011 | 00001010
-----+--------^--------------+---------
 -12 | ...101010100111010111 | 00010101
-----+--------^--------------+---------
  -8 | ..1010101001110101111 | 00101010
-----+--------^--------------+---------
  -4 | .10101010011101011111 | 01010101
-----+--------^--------------+---------
   0 | 101010100111010111111 | 10101010
-----+--------^--------------+---------
  +4 | 010101001110101111111 | 10101010
-----+--------^--------------+---------
  +8 | .......11101011111111 | 00000001
-----+--------^--------------+---------
 +12 | ......111010111111110 | 00000011
-----+--------^--------------+---------
 +16 | .....1110101111111101 | 00000111
-----+--------^--------------+---------
 +20 | ....1110101111111101. | 00001110
-----+--------^--------------+---------

At T+0, the high bit of the data latch
goes to 1 for the first time, so the
LDA/BPL loop at $2876 will exit. After
that, it's literally a race against
time, because the disk keeps spinning
(and bits keep coming) independently of
the CPU.

At T+4, the disk sees the extra 0 bit
(timing bit) after the $AA nibble. This
does not change the value of the data
latch; it "holds" its value until it
sees a new 1 bit. If the timing bit had
not been present, the data latch would
have seen a 1 bit here instead (the
high bit of every nibble is always 1),
which would have caused the data latch
to reset and start accumulating the new
value ($01). But because the timing bit
is present, that reset gets delayed by
4 CPU cycles.

At T+8, the first bit of the $EB nibble
shows up. This is a 1, so now the data
latch resets and starts accumulating
the new value ($01).

At T+12, the second bit of the $EB
nibble shows up. This is also a 1. All
the bits of $FF are 1. It gets shifted
into the data latch, which is now $03.

At T+16, the third bit of $EB shows up.
The data latch is now $07.

At T+20, the 4th bit of $EB shows up.
The data latch is now $0E.

Thus, the "race against time" looks
like this:

      ---$C0EC-data-latch--
Time | original |     copy | identical?
-----+----------+----------+-----------
 -28 | 00000001 | 00000001 |    yes
 -24 | 00000010 | 00000010 |    yes
 -20 | 00000101 | 00000101 |    yes
 -16 | 00001010 | 00001010 |    yes
 -12 | 00010101 | 00010101 |    yes
  -8 | 00101010 | 00101010 |    yes
  -4 | 01010101 | 01010101 |    yes
   0 | 10101010 | 10101010 |    yes
  +4 | 10101010 | 00000001 |    NO!
  +8 | 00000001 | 00000011 |    NO!
 +12 | 00000011 | 00000111 |    NO!
 +16 | 00000111 | 00001110 |    NO!
 +20 | 00001110 | 00011101 |    NO!

As you can see, there is a short window
of time -- after you read a nibble from
disk but before the next nibble has
fully shifted into place -- where the
value of the data latch will indicate
whether the previous nibble had a
timing bit after it. That window is the
heart of the protection scheme.

Timing bits are easy to write to disk,
if you know where you want them to go.
(You literally do nothing for 4 CPU
cycles after writing a nibble to disk.)
But early bit copy programs could not
preserve timing bits on copies they
made. (This was a major advancement in
later versions of EDD and Copy II Plus,
at which point copy protection schemes
switched to requiring a mixture of one
timing bit in some places and two in
others -- again pushing beyond the
boundaries of what bit copy programs
could reproduce.)

But here, the presence of a single
timing bit is the indicator that the
disk is an original. And the absence of
a timing bit means the disk is an
unauthorized copy.

                   ~

               Chapter 4
      In Which We Ignore All That
    And Just Bypass The Damn Thing


There are no side effects within the
protection check itself, but there is
some code before we get there: clearing
part of the text page and setting a
singular address in zero page. I'm
going to play it safe and keep those
intact. But I can change the first
instruction of the protection check to
unconditionally JMP $2000, to the start
of the game code.

A quick search in a sector editor finds
the protection code on track $20.

T20,S01,$07: 20E303 -> 4C0020

]PR#6
...works...

Quod erat liberandum.

                   ~

               Changelog


2018-04-07

- fixed "as the disk spins" chart typo
  (thanks @0x3d0g)

2018-04-06

- initial release

---------------------------------------
A 4am crack                    No. 1733
------------------EOF------------------
